Copyright: Copyright 1996 by PowerBASIC, Inc.


Contents
PB/DLL Debugger Help

Table of contents

Introduction 	Debugging DLLs

Starting a session 	BreakPoints

Stepping and Tracing 	The Watch Window

The Evaluate Window 	Writing Better Programs

Debugging checklist 	Change management

Design for the future 	Debug low-level code first

Look for bug types 	Get it right the first time

Summary


Introduction

The PowerBASIC DLL Debugger for Windows (PBDW) can be used to find and correct both general programming errors and errors specific to Basic.  Nearly every program has bugs, at least at first.  To find them, you may need to check any statement in the program, display the value of any variable, and observe the program flow from line to line.  PBDW has all these capabilities, and more.

This help file explains how to use PBDW to find and fix errors in a sample DLL by providing a list of the debugging commands, a description of each, and show how each is invoked.  If you follow certain guidelines when creating your program, you'll find debugging easier (and less necessary).  The procedures we describe here will help you form your own set of guidelines that will make your programs easier to write and maintain.

Debugging DLLs


Debugging DLLs

Debugging a DLL presents a singular challenge.  Unlike a program, a DLL doesn't have a known location where execution begins or a known location where execution ends.  In fact, a DLL doesn't do anything until an application requests one of its services.  So, in order to debug a DLL, it is necessary to have one or more programs requesting its services (calling its Subs and Functions).  For the sake of debugging, PBDW takes the place of the application(s) to allow you to invoke and test individual routines in your DLL.

In order to debug a DLL created with PB/DLL, you need to include the $DEBUG PBD ON meta-statement at the begining of your source module, before any executable statements.  This tells PB/DLL to include symbolic information in the DLL for use with the stand-alone PBDW debugger.  A DLL which has debugging information in it can not be called by any other application.  You must also include an exported sub called PbDebug which has no parameters.  This sub will contain any code necessary to call the Sub or Function in your DLL which you wish to debug.  Example:

$DEBUG   PBD ON

$COMPILE DLL

SUB PBDebug () EXPORT

  CALL MySub(Arg%, Arg2$)

END SUB

SUB MySub(Arg1%, Arg2$) EXPORT

  .

  .

  .

END SUB

Starting a debugging session


Starting a session
Starting a debugging session

To debug a DLL using the PBDW debugger, first launch the debugger by double-clicking on its icon.  You will then see five windows on the screen.  The top window is the main window and contains the control menu and buttons for the most common operations.  Below the main window and to the left is the source window.  This is where you set breakpoints, select variables to watch, etc.  To the top right of the source window is the watch window, where the watched variables are displayed.  Along the bottom left of the screen is the Debug Print window where text from $DEBUG PRINT calls in your DLL are displayed.  Finally, at the bottom right is the CPU watch window where the CPU register values are displayed.

Click on the "File" menu and select "Open", or click on the "Open" button.  You will then get a file dialog.  Select the DLL you wish to debug.  The source code for the DLL as well as its symbol file (.PBD) must be located in the same directory as the DLL, otherwise you'll receive an error message after the DLL is selected.  Once selected DLL will be loaded into memory along with its associated source file which is then displayed in the source window.

Setting and using Breakpoints


BreakPoints
Setting and using breakpoints

Breakpoints are user-designated locations (lines) in your code where execution will halt.  In most cases, you'll want to set one or more breakpoint(s) in your program before you run it.  The program executes up to (but not including) the line containing the breakpoint, then passes control of the debugger over to you.  Breakpoints you set remain in effect until you clear them or until you exit the debugger.

To set a breakpoint at a line in your code, position the cursor on the line in question.  Choose the Debug/Breakpoint menu option or press the corresponding hot key, F9.  The line is highlighted, indicating that the breakpoint has been set.  If you wanted to remove a breakpoint at that line, choosing the Debug/Breakpoint menu option would make the existing highlighting disappear.  That method of operation is called a toggle: selecting the operation reverses whatever the current state is.  The color used for breakpoint highlighting differs from that used for the execution bar.  This color difference helps avoid any confusion over highlighted breakpoints and the execution bar.

The breakpoint line can't be doubly highlighted, so the execution bar obscures the breakpoint highlighting until execution moves past the breakpoint line.  If a code segment containing a breakpoint is repeatedly executed, the program will stop each time execution reaches the breakpoint line.

Once at a breakpoint, you can:

Display the value of a variable or certain expressions

Set up a list of expressions in a "watch" window and see how their values change as the program executes

Change the value of a variable

Clear breakpoints, set new ones, or both

Single-step the program (run it one line at a time)

Run the program to the next breakpoint

Stepping and Tracing


Stepping and Tracing

Once the DLL is loaded you can begin stepping through the DLL by pressing F8 or using the mouse to select the Step button in the toolbar.  This causes the debugger to execute the currently highlighted source code line and move the highlight bar to the next executable line of code and stop.

If you encounter a source code line which makes a call to another Sub or Function in the same DLL you can step into that Sub or Function by presseng Shift-F8 or selecting the Trace button in the toolbar.

Pressing the F9 key or selecting the Run button on the toolbar will cause execution to continue until the next breakpoint is reached.  If no breakpoints are encountered, execution continues to the end of the code.

The "Watch" window


The Watch Window
The "Watch" window

The Watch window contains a list of variables which you want to watch the values of.  For example, you may have the variable X in your which you want to watch to see at which point its value becomes invalid in your code.  Pressing Ctrl-W or selecting the Watch button on the toolbar will allow you to add or edit variables in the Watch window.  Double-clicking on a variable in the watch window will pop up a dialog box which will allow you to delete the variable from the watch window, or change its value while the DLL is executing.

A value of "????" is returned if the variable does not exist, or if it is not defined in the current Sub or Function.

The CPU watch window displays the values of the CPU registers after each line executes.

The "Evaluate" Window


The Evaluate Window
The "Evaluate" window

The "Evaluate" command evaluates a variable, displays its value, and if appropriate, lets you modify the value.  You invoke the Evaluate command by either pressing Control-E, or clicking on the Eval button.  Either action brings up the "Evaluate" window, which contains two edit fields and three buttons.  The first edit field is where the variables name.  The second edit field is the Result / New Value box.  This is where the variable's current value is displayed.  This is also where you would enter a new value to be assigned to that variable.  When a new value is assigned to a variable, the the change is automatically reflected in the "Watch" window.  The three buttons are:

Evaluate

    Evaluates or re-evaluates the variable's value and updates the result box.

Set New Value

    Assigns the value in the NewValue box, to the variable.

Done

    Closes the Evaluate window.

Writing Better Code


Writing Better Programs
Writing better code

There's a lot more to writing robust programs than simply dashing down some code and then letting the debugger work on it.  The debugger is always a useful tool, but is more so when the code is designed carefully, and modifications and additions are planned for.  Good programs do not happen by accident.

In this section, we'll consider some things you can do to make your programs easier to debug.  As a side benefit, the changes will also make your programs need the debugger less often, and make them easier to maintain.  These guidelines are especially important for BASIC programs.  Other languages rigidly enforce rules that tend to prevent certain kinds of mistakes (often at the expense of flexibility), but BASIC gives the programmer enough freedom to get into some serious trouble.

Debugging checklist


Debugging checklist

When you debug a program, you may have a specific set of things you prefer to test and a preferred order for testing them.  There are no set rules for this, either.  It all depends on the kinds of programs you write and your level of expertise.  However, regardless of the kind of work involved, you may find the following checklist of some value:

1.	Run the program using simple data first.  If the code won't handle simple data, it doesn't matter if it can handle anything else.  Start with trivial cases and proceed from there.  If you find bugs, correct them until all simple cases are processed correctly.

2.	Choose extreme data next.  Use the debugger to force extreme values for certain variables.

3.	Consider what might happen if a program "falls through" a test.  Many programmers present a Y/N choice for operator input and check for only one or the other input.  What if the character entered were neither Y nor N?  It would be useless to branch to one routine if the answer were Y, to another if the answer were N, and do no branch if the answer were neither.  (Hint: Using a DO...LOOP UNTIL loop to repeat the prompt until the user enters an appropriate answer is one way around this particular problem.)

4.	Never trust that input or data from a disk file will be correct.  Even if you have warned the user not to do something, never assume that the user won't do it.  Even if a file should not contain a particular kind of data, don't assume that sort of data will not be present.  Instead, make sure all possible cases are handled.

5.	Examine all subs and functions for incorrect exits.  GOSUB subroutines can be exited only with a RETURN statement.  SUBs can only be exited with an END SUB or EXIT SUB.  FUNCTIONs can only be exited with an END FUNCTION or EXIT FUNCTION.  You can not exit a SUBor FUNCTION with a RETURN.

6.	Be careful in how you use GOTO.  There is nothing wrong with GOTO.  However, a GOTO to another GOTO or a GOTO to a GOSUB is considered poor programming.

7.	Spread your code out.  In PB/DLL, there is no need to pack many statements on a single line; you save no memory and gain no speed advantage.  In contrast, spreading your code out makes it easier for you when you use the debugger because you can stop the program in more places and get a better idea of the results of operations.  Unless you are sure that you will never write a program with a bug in it, the easier a program is to debug, the better your life will be.

8.	Give your program to someone else to test.  In commercial programming environments, this is called beta testing.  Code you have written yourself will often contain false assumptions.  You can't test those assumptions if you're not aware of them.  Someone else won't make those same assumptions, and will therefore be more likely to discover bugs.  Have the beta testers carefully document exactly what inputs they use and what error messages or improper results occurred.  If that's not possible, you may want to sit beside a beta tester and write down the inputs and errors yourself.  One good beta tester would be the intended user, or someone as much like that user as possible.  Another good beta tester is an experienced programmer: tell that person to make every attempt to "break" the program.

9.	Watch for misspelled variable names.  Use the $DIM ALL or OPTION EXPLICIT options or a cross-reference program.  You might also use an editor to make a global change in the variable name, then look for occurrences of fragments of the old name.  This trick can be handy if you want to look at the whole program as rapidly as possible.

10.	Avoid unnecessary implicit type conversions.  Do you know what happens when you assign the value 1.4343 to an integer?  What about -1.4343?  If you do know and have made allowances for this, you will be all right.  If you don't know, you shouldn't do it.

11.	Avoid logical tests for equality on the value of floating-point numbers.  Instead of branching if the value of a floating-point number is some exact value, make the branch if the value is close to that exact value.  Even though a number's value may seem to be exactly 2, it may be stored as 1.999999999999.  If that's the case, the test for equality will fail.  This sort of error is more likely if the variable's value was calculated.

12.	Use as few labels and line numbers as possible.  This makes your programs much easier to debug.  The fewer labels and line numbers you use, the more likely you will have fewer GOTO and GOSUB statements, and the more likely your program will be modular.  The fewer times you change the program's flow, the easier the program will be to read, understand, modify, and debug.  To that end, take advantage of PB/DLL's EXIT statements whenever possible--they were added to reduce the number of labels you might otherwise have to use.

The following sections discuss some more guidelines in detail.

Change management


Change management

Before you modify any program, save a copy of the unmodified original. If you work yourself into a corner, it's always nice to have something correct to which you can return.

When you make modifications, test not only the modifications, but also all parts of the program affected by them. This is where functions and procedures are especially nice; if the changes you make affect only one part of the program, then only that part needs to be retested.

When the modifications are complete, document exactly what changed, for what reason, and when.  If you're working as part of a programming team, you should also note who made the change (and possibly at whose request).  As a part of the documentation, you should also note how the change was tested, with what kinds of data, and by whom.  If bugs arise later, this data will help you find better tests and procedures for future programs.

Design for the future


Design for the future

Many program bugs come about because too many programmers equate trickiness with intelligence.  No one likes to be thought stupid, but trickiness can result in a lot of problems.  Consider: the routine you're so proud of may be the same one you must understand and modify six months or a year from now.  If the routine were written more conservatively, would it hurt the program's performance?  If the routine were carefully commented, wouldn't you thank yourself later?

The idea is not just to write a program and be done with it, but to write a program you can read, fix, and enhance later on.

There is no reason to avoid long variable names. If such names bother you when you write the program, use shorter names and then do a global search and replace to a more descriptive one before the program is saved. That way, you'll save typing but still have something readable when you're done. If you do make a global change, it may also serve as a way to find misspelled variable names.

Try to use functions, procedures, and units as much as possible. Further, accumulate (or buy) libraries of procedures. These procedures will be tested far more intensively because they will have been used far more often. The less you have to test, the faster and easier it will be to debug.

Try to avoid using procedures and functions which require long lists of parameters. The simpler a procedure is, the less you have to keep track of when debugging. If a procedure does seem to require a long list of parameters, it's likely that you have two procedures combined into one. Split the procedures into logical units where possible.

When you write a program, go for functionality first, and optimization later, when you have a program that works.  A program that runs at blinding speed and produces incorrect output might as well not run at all.  Besides, you won't know which parts of the program will benefit most from optimization until you've written it.  Once it's written, you'll have a better handle on which routines are in the critical path and therefore could benefit from being optimized.

Bottom-up debugging


Debug low-level code first
Debug one procedure at a time

When debugging a Sub or Function in a DLL, step-over any other procedures which you may be calling until the top level Sub or Function is debugged first.  Attempting to debug several levels of procedures calling other procedures can be more difficult.

Bug categories


Look for bug types
Categorize your bugs

One time-saving debugging technique involves looking for specific bug types or categories.  Perhaps you reversed the order of arguments for the STRING$ or INSTR function.  Ordinarily, you may write that code correctly, but just this time you put them in backwards.  If you find an error of a certain type, look through your program for more instances of it.  Chances are, that critter has a few relatives.

Get it right the first time


Get it right the first time
Try to get it right the first time

This is the fundamental rule for programming.  There's no reason to write sloppy code.  There is just as little reason to write code you are pretty sure won't work.  If you think it won't work, why write it?

Most of the time, this impulse strikes when the section of code seems very complex.  If that's the case, take your time and write the code as simply as you can.  Even if you know the code won't handle all cases, at least write it so it handles a part of the problem without errors.  When you've finished the first attempt, then go back and think about the modifications you need to handle other cases.

Summary


Summary

To sum up: If you run into a problem you can't solve, break it down into something more manageable, and work from there.  You'll be surprised how difficult programming projects become easy, or at least more approchable.